package com.nulldev.util.JVM.reflections;

import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.JarURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import com.nulldev.util.JVM.JVM;
import com.nulldev.util.io.file.FileUtil;

public class ReflectionClasses {
	/**
	 * Private helper method
	 * 
	 * @param directory The directory to start with
	 * @param pckgname  The package name to search for. Will be needed for getting
	 *                  the Class object.
	 * @param classes   if a file isn't loaded but still is in the directory
	 * @throws ClassNotFoundException
	 */
	private static void checkDirectory(File directory, String pckgname, List<Class<?>> classes) throws ClassNotFoundException {
		File tmpDirectory;

		if (directory.exists() && directory.isDirectory()) {
			final String[] files = directory.list();
			for (final String file : files) {
				if (file.endsWith(".class")) {
					try {
						classes.add(ClassCache.forName(pckgname + '.' + file.substring(0, file.length() - 6)));
					} catch (final NoClassDefFoundError e) {
						// do nothing. this class hasn't been found by the
						// loader, and we don't care.
					}
				} else if ((tmpDirectory = new File(directory, file)).isDirectory()) {
					checkDirectory(tmpDirectory, pckgname + "." + file, classes);
				}
			}
		}
	}

	/**
	 * Private helper method.
	 * 
	 * @param connection the connection to the jar
	 * @param pckgname   the package name to search for
	 * @param classes    the current ArrayList of all classes. This method will
	 *                   simply add new classes.
	 * @throws ClassNotFoundException if a file isn't loaded but still is in the jar
	 *                                file
	 * @throws IOException            if it can't correctly read from the jar file.
	 */
	private static void checkJarFile(JarURLConnection connection, String pckgname, List<Class<?>> classes) throws ClassNotFoundException, IOException {
		final JarFile jarFile = connection.getJarFile();
		final Enumeration<JarEntry> entries = jarFile.entries();
		String name;

		for (JarEntry jarEntry = null; entries.hasMoreElements() && ((jarEntry = entries.nextElement()) != null);) {
			name = jarEntry.getName();

			if (name.contains(".class")) {
				name = name.substring(0, name.length() - 6).replace('/', '.');

				if (name.contains(pckgname)) {
					classes.add(ClassCache.forName(name));
				}
			}
		}
	}

	/**
	 * Attempts to list all the classes in the specified package as determined by
	 * the context class loader
	 * 
	 * @param pckgname the package name to search
	 * @return a list of classes that exist within that package
	 * @throws ClassNotFoundException if something went wrong
	 */
	public static List<Class<?>> getClassesForPackage(String pckgname) throws ClassNotFoundException {
		final ArrayList<Class<?>> classes = new ArrayList<Class<?>>();

		try {
			final ClassLoader cld = Thread.currentThread().getContextClassLoader();

			if (cld == null)
				throw new ClassNotFoundException("Can't get class loader.");

			final Enumeration<URL> resources = cld.getResources(pckgname.replace('.', '/'));
			URLConnection connection;

			for (URL url = null; resources.hasMoreElements() && ((url = resources.nextElement()) != null);) {
				try {
					connection = url.openConnection();

					if (connection instanceof JarURLConnection) {
						checkJarFile((JarURLConnection) connection, pckgname, classes);
					} else if (connection.getClass().getCanonicalName().equals("sun.net.www.protocol.file.FileURLConnection")) { // (connection instanceof
																																	// FileURLConnection) {
						try {
							checkDirectory(new File(URLDecoder.decode(url.getPath(), "UTF-8")), pckgname, classes);
						} catch (final UnsupportedEncodingException ex) {
							throw new ClassNotFoundException(pckgname + " does not appear to be a valid package (Unsupported encoding)", ex);
						}
					} else
						throw new ClassNotFoundException(pckgname + " (" + url.getPath() + ") does not appear to be a valid package");
				} catch (final IOException ioex) {
					throw new ClassNotFoundException("IOException was thrown when trying to get all resources for " + pckgname, ioex);
				}
			}
		} catch (final NullPointerException ex) {
			throw new ClassNotFoundException(pckgname + " does not appear to be a valid package (Null pointer exception)", ex);
		} catch (final IOException ioex) {
			throw new ClassNotFoundException("IOException was thrown when trying to get all resources for " + pckgname, ioex);
		}

		return classes;
	}

	public static List<Class<?>> getClasses(final String... packages) throws ClassNotFoundException {
		final ArrayList<Class<?>> cls = new ArrayList<Class<?>>();
		for (final String s : packages) {
			cls.addAll(getClassesForPackage(s));
		}
		return cls;
	}

	public static Map<String, File> queryItemsFolder(final String path) throws IOException {
		final Map<String, File> out = new HashMap<String, File>();
		final File jarFile = new File(JVM.class.getProtectionDomain().getCodeSource().getLocation().getPath());

		if (jarFile.isFile()) {
			final JarFile jar = new JarFile(jarFile);
			final Enumeration<JarEntry> entries = jar.entries();
			while (entries.hasMoreElements()) {
				final JarEntry ent = entries.nextElement();
				final String name = ent.getName();
				if (name.startsWith(path + "/") && name.contains(".")) {
					// out.add(extract(name));
					// System.out.println("Found >> " + "/" + FileUtil.getFileName(name));
					out.put(FileUtil.getFileName(name), FileUtil.getResourceAsFile(ReflectionClasses.class, "/" + name));
				}
			}
			jar.close();
		} else {
			final File url = FileUtil.getResourceAsFile(ReflectionClasses.class, "/" + path);
			if (url != null) {
				try {
					final File apps = url;
					for (File app : FileUtil.listFilesForFolder(apps)) {// apps.listFiles()) {
						out.put(app.getName(), app);
					}
				} catch (Exception ex) {
					ex.printStackTrace();
				}
			}
		}

		return out;
	}
}
